from os import path, listdir, getcwd
import sys
import io, base64
import numpy as np
import matplotlib.pyplot as plt
from IPython.display import Image, HTML
videos_dir = path.join('..', 'dados', 'pendulo_movimento')
movimento_dir = path.join('..', 'codigos', 'deteccao_de_movimento')
Não obtive sucesso com o opencv no binder. Como estão, as notas podem funcionar localmente, no jupyter lab ou jupyter notebook, e no Google Colab. Para usá-las no google colab, é preciso executar as seguintes células, com colab = True.
colab = False
if colab:
!git clone https://github.com/rmsrosa/modelagem_matematica.git
if colab:
videos_dir = path.join('modelagem_matematica', 'dados', 'pendulo_movimento')
movimento_dir = path.join('modelagem_matematica', 'codigos', 'deteccao_de_movimento')
O módulo python de detecção de movimento foi feito com base no código descrito na página Basic motion detection and tracking with Python and OpenCV.
Esse código utiliza o pacote OpenCV do python e ferramentas de manipulação de imagens do próprio autor do script, contidas no pacote imutils (disponível em github/jrosebr1/imutils e em PyPI - imutils).
O código original motion_detector.py na página Basic motion detection and tracking with Python and OpenCV abre três janelas de vídeo e, por conta dessas janelas, não funciona dentro do Jupyter.
Fiz modificações para ele funcionar como um módulo a ser importado pelo Jupyter.
No código original, o movimento é apenas exibido em um vídeo.
Modifiquei-o para guardar esse movimento em uma lista, contendo o número de cada quadro analisado e os dados espaciais da caixa de entorno do objeto em movimento, no quadro em questão.
Os dados da caixa de entorno contém as coordenadas horizontal e vertical (em pixels) do canto superior esquerdo da caixa (retângulo) que contém o objeto detectado, seguidas da largura e da altura da caixa.
Observação: É importante ressaltar, na intepretação do resultado, que o ponto de coordenada $(0,0)$ corresponde ao canto superior esquerdo.
Primeiro, verifico o diretório do módulo.
listdir(movimento_dir)
['.ipynb_checkpoints', '__pycache__', 'movimento.py']
Em seguida, adiciono este caminho ao PATH e importo o módulo
sys.path.insert(0, path.abspath(path.join(getcwd(),movimento_dir)))
import movimento
help(movimento)
Help on module movimento:
NAME
movimento - Detecção e traçamento do movimento de objetos em vídeos.
DESCRIPTION
Este código possui uma função de detecção e traçamento do
movimento de objetos em vídeos.
O código identifica objetos se movendo e enquadra o objeto com uma
"caixa de entorno". Para cada quadro em que um objeto em movimento,
o número do quadro, junto com os dados dessa caixa de entorno
(coordenadas horizontal e vertical do canto inferior da caixa e a
largura e a altura da caixa) são registrados em uma lista. Essa
lista é retornada pela função ao final da análise.
Isso é implementado por uma única função, que possui outros argumentos
de configuração e funcionalidades:
* detector_de_movimento: recebe o caminho para um arquivo de video
que será analisado visando detectar e traçar o movimento de um
ou mais objetos.
Retorna o número de quadros por segundo do video e os dados
sobre o andamento da caixa de entorno dos objetos em movimento.
Dependendo dos argumentos, pode gravar em disco ou exibir
na tela os videos usados ao longo do processamento. Outras
opções ajustam a área mínima para um objeto ser identificado,
o nível de corte da escala de cinzas usada na detecção do
objeto e os quadros inicial e final para o tratamento.
Para mais informações, veja o `help` da função `detector_de_movimento`.
Também é possível usar o código como um script, a partir da linha de
comando. Veja as informações sobre como passar os argumentos através do
comando de linha
```bash
detector_de_movimento --help
```
Exemplos de uso como script:
1) Exibindo, na tela do computador, os vídeos de processamento
$ python movimento.py --video video.mov -e
2) Alterando alguns parâmetros, gravando o video processado e
exibindo os vídeos de processamento:
$ python movimento.py --video video.mov -o video_tratado.avi
--area_min 200 --nivel_de_corte 50 -e
O código usa o pacote `opencv` e o módulo `imutils`, além do módulo
padrão `argparse`.
Baseado no script disponível em [Basic motion detection and tracking with Python and OpenCV](https://www.pyimagesearch.com/2015/05/25/basic-motion-detection-and-tracking-with-python-and-opencv/).
FUNCTIONS
detector_de_movimento(video, width=512, area_min=500, nivel_de_corte=25, quadro_inicial=1, quadro_final=-1, video_tracado=None, video_cinza=None, video_pb=None, exibe_videos=False)
Detecta e traça o movimento de objetos em vídeos.
Esta função usa um método simples de contraste de cada quadro
com o quadro inicial para detectar a alteração ocorrida e
inferir e acompanhar a posição de um objeto que se moveu ou
que não estava presente na imagem original.
Se o objeto já aparece na imagem inicial e se desloca, um
"fantasma" do objeto é constantemente detectado. O ideal é que
o objeto não apareça inicialmente no vídeo.
O algoritmo de detecção é baseado nos seguintes passos:
- O primeiro quadro do filme é transformado em escala
de cinza (de 0 a 255, i.e. de preto a branco), gerando
um quadro denominado `firstFrame`.
- Cada quadro subsequente também é transformado em escala
de cinza (denominado `gray`) e um novo quadro é formado com
a diferença entre esse quadro e o primeiro quadro:
`tresh = gray - firstFrame`.
- Com base em um `nivel_de_cinza`, entre 0 e 255, essa
diferença é transformada em uma imagem `tresh` em preto
e branco, ficando cada *pixel* preto, se o cinza estiver
abaixo desse nível, ou branco, se estiver acima.
- Curvas de contorno entre as partes em preto e em branco
da imagem `tresh` são identificadas pelo `opencv`.
- Para cada curva envolvendo uma área maior do que uma área
mínima `area_min`, uma *caixa de entorno* é registrada.
Entrada:
--------
video: string
Uma string com o nome (incluindo o caminho) do arquivo
de vídeo a ser tratado.
area_min: ponto flutuante
A área mínima (em número de pixels) necessária para
que um objeto seja identificado. Por exemplo, um
pequeno retângulo de 20 por 20 pixels tem 400 pixels
de área.
nivel_de_corte: ponto flutuante
O nível de corte para o contraste, em uma escala de 0 a 255,
para determinar, em cada pixel da imagem, se há diferente
entre o quadro atual e o quadro inicial ou não.
video_tracado: string
Uma string com o nome (incluindo o caminho) do arquivo
de vídeo a ser gravado contendo o resultado do
processamento. O vídeo inclui textos indicando o número
de cada quadro, o tempo decorrido, o estado do ambiente
(se ocupado ou desocupado) e com uma caixa de contorno
em volta de cada objeto detectado.
video_cinza: string
Uma string com o nome (incluindo o caminho) do arquivo
de vídeo a ser gravado contendo a diferença (pixel a pixel)
entre os níveis de cinza entre o quadro atual e o quadro
inicial, que forma a base para a detecção.
video_pb: string
Uma string com o nome (incluindo o caminho) do arquivo
de vídeo a ser gravado, em preto e branco, contendo
a versão "estourada" do `video_cinza` descrito acima.
O estouro é feito com base no `nivel_de_cinza`.
exibe_videos = Boolean
Se `True`, exibe os vídeos envolvidos no tratamento, a
saber, um vídeo com o contraste inicial entre o quadro
atual e o inicial, um vídeo com esse contraste saturado
e um vídeo com o processamento final, descrito na
opção `video_tracado`.
Saída:
------
fps: ponto flutuante
O número de quadros por segundo do vídeo.
num_quadros: ponto flutuante
O número total de quadros examinados.
tracado: lista
Uma lista de listas, onde o primeiro item da lista
é a lista de strings ['n', 'x', 'y', 'l', 'h'], o segundo
item é a lista de strings `['quadro', 'abscissa(px)',
'ordenada(px)', 'largura(px)', 'altura(px)']` e outros
itens são listas contenda, no ordem, inteiros indicando
o número do quadro correspondente, a abscissa e a ordenada
do cando esquerdo inferior da caixa de entorno do objeto
identificado no quadro e a largura e a altura dessa caixa
de entorno.
Quando mais de um objeto é identificado em um mesmo quadro,
os dados da caixa de entorno de cada objeto são incluídos
em itens diferentes da lista `tracado`, contendo o mesmo
número de quadro.
Exemplos:
--------
1) Gravando o vídeo processado:
detector_de_movimento("video.mp4",
video_tracado = "video_tratado.avi")
2) Alterando alguns parâmetros e exibindo os vídeos de
processamento:
detector_de_movimento("video.mp4", area_min = 200,
nivel_de_corte = 50,
exibe_videos = True)
DATA
__homepage__ = 'http://github.com/rmsrosa/jupyterbookmaker'
__license__ = 'GNU GPLv3'
__status__ = 'beta'
VERSION
0.3.1
AUTHOR
Ricardo M. S. Rosa <rmsrosa@gmail.com>
FILE
/Users/rrosa/Dropbox/Documents/math-atual/im/modmat-2019/modmatrepository/codigos/deteccao_de_movimento/movimento.py
A captura do movimento é feita através de um simples método de contraste com a imagem do primeiro quadro do filme. Os seguintes passos são feitos:
O primeiro quadro do filme é transformado em escala de cinza (de 0 a 255, i.e. de preto a branco), gerando um quadro denominado firstFrame.
Cada quadro subsequente também é transformado em escala de cinza (denominado gray) e um novo quadro é formado com a diferença entre esse quadro e o primeiro quadro: tresh = gray - firstFrame.
Com base em um nivel_de_cinza, entre 0 e 255, essa diferença é transformada em uma imagem tresh em preto e branco, ficando cada pixel preto, se o cinza estiver abaixo desse nível, ou branco, se estiver acima.
Curvas de contorno entre as partes em preto e em branco da imagem tresh são identificadas pelo opencv.
Para cada curva envolvendo uma área maior do que uma área mínima area_min, uma caixa de entorno é registrada.
Foram três vídeos gravados, dois de pêndulos com $70\,\texttt{cm}$ de haste e um com $40\,\texttt{cm}$.
Estão no subdiretório dados/pendulo_movimento.
listdir(videos_dir)
['.DS_Store', 'pendulo_40cm_reduzido.mov', 'pendulo_40cm_reduzido_tracado_inicio.avi', 'pendulo_70cm_1_reduzido.mov', 'pendulo_70cm_1_trecho.mov', 'pendulo_70cm_1_trecho_cinza.avi', 'pendulo_70cm_1_trecho_com_deteccao.avi', 'pendulo_70cm_1_trecho_pb.avi', 'pendulo_70cm_2_reduzido.mov']
Vamos começar com o vídeo pendulo_70cm_1_trecho.mov, que exibe apenas um trecho inicial, de uns 4 segundos, do vídeo pendulo_70cm_1_reduzido.mov, que tem 16 segundos. O termo reduzido se refere a reduções na resolução e na qualidade do vídeo original, que é de 44Mb. O original do vídeo seguinte, com 2 minutos, tem mais de 230Mb. Por isso a necessidade dessa redução.
HTML(data='''<video width="80%" alt="test" controls><source src="data:video/mp4;base64,{0}" type="video/mp4" />
</video>'''.format(encoded.decode('ascii')))
Agora, executo o módulo neste vídeo, guardando os dados do movimento em uma variável dados e gravando o vídeo de saída no arquivo
fps, num_quadros, tracado = movimento.detector_de_movimento(path.join(videos_dir, 'pendulo_70cm_1_trecho.mov'),
video_tracado=path.join(videos_dir, 'pendulo_70cm_1_trecho_com_deteccao.avi'),
video_cinza=path.join(videos_dir, 'pendulo_70cm_1_trecho_cinza.avi'),
video_pb=path.join(videos_dir, 'pendulo_70cm_1_trecho_pb.avi'))
print('Quadros por segundo:', fps)
print('Número de quadros:', num_quadros)
print(f"Tempo tratado de vídeo: {int(num_quadros/fps/60):3d}m {int(num_quadros/fps % 60):02d}s {int(1000*(num_quadros/fps % 1)):02d}ms")
print()
print('Dados iniciais do traçado:')
tracado[1:11]
Quadros por segundo: 29.97002997002997 Número de quadros: 119 Tempo tratado de vídeo: 0m 03s 970ms Dados iniciais do traçado:
[['quadro', 'abscissa(px)', 'ordenada(px)', 'largura(px)', 'altura(px)'], [16, 495, 107, 17, 49], [17, 494, 107, 18, 51], [18, 493, 107, 19, 51], [19, 493, 107, 19, 51], [20, 493, 107, 19, 50], [21, 493, 107, 19, 51], [22, 493, 108, 19, 50], [23, 493, 108, 19, 49], [24, 492, 107, 20, 51]]
video = io.open(path.join(videos_dir, 'pendulo_70cm_1_trecho_com_deteccao.avi'), 'r+b').read()
encoded = base64.b64encode(video)
HTML(data='''<video width="80%" alt="test" controls><source src="data:video/avi;base64,{0}" type="video/avi" />
</video>'''.format(encoded.decode('ascii')))
video = io.open(path.join(videos_dir, 'pendulo_70cm_1_trecho_cinza.avi'), 'r+b').read()
encoded = base64.b64encode(video)
HTML(data='''<video width="80%" alt="test" controls><source src="data:video/avi;base64,{0}" type="video/avi" />
</video>'''.format(encoded.decode('ascii')))
video = io.open(path.join(videos_dir, 'pendulo_70cm_1_trecho_pb.avi'), 'r+b').read()
encoded = base64.b64encode(video)
HTML(data='''<video width="80%" alt="test" controls><source src="data:video/avi;base64,{0}" type="video/avi" />
</video>'''.format(encoded.decode('ascii')))
A haste do pêndulo é de $70\,\texttt{cm}$ contados da ponta de cima do fio até a centróide da bola, considerada como sendo o centro de massa do objeto, que por sua vez é tratado como um objeto pontual.
Primeiro, lemos os dados retornados pelo traçador:
nd = np.array(tracado[2:])
quadro = nd[:,0]
x = nd[:,1]
y = nd[:,2]
w = nd[:,3]
h = nd[:,4]
plt.figure(figsize=(12,4))
plt.plot(quadro, x, '.', label='lado esquerdo')
plt.plot(quadro, x+w, '.', label='lado direito')
plt.plot(quadro, x+w/2, '-', label='média')
plt.title('Coordenada horizontal da caixa de entorno', fontsize=18)
plt.legend()
plt.show()
plt.figure(figsize=(12,4))
plt.plot(quadro, y, '.', label='lado superior')
plt.plot(quadro, y+h, '.', label='lado inferior')
plt.plot(quadro, y+h/2, '-', label='média')
plt.ylim(plt.ylim()[::-1])
plt.title('Dados verticais da caixa de entorno', fontsize=18)
plt.legend()
plt.show()
Agora, não vamos exibir os vídeos, apenas capturar os dados do vídeo completo.
fps1, num_quadros1, tracado1 = movimento.detector_de_movimento(path.join(videos_dir, 'pendulo_70cm_1_reduzido.mov'))
print('Quadros por segundo:', fps1)
print('Número de quadros:', num_quadros1)
print(f"Tempo tratado de vídeo: {int(num_quadros1/fps1/60):3d}m {int(num_quadros1/fps1 % 60):02d}s {int(1000*(num_quadros1/fps1 % 1)):02d}ms")
print()
print('Dados iniciais do traçado:')
tracado1[1:11]
Quadros por segundo: 29.97002997002997 Número de quadros: 490 Tempo tratado de vídeo: 0m 16s 349ms Dados iniciais do traçado:
[['quadro', 'abscissa(px)', 'ordenada(px)', 'largura(px)', 'altura(px)'], [16, 495, 107, 17, 49], [17, 494, 107, 18, 51], [18, 493, 107, 19, 51], [19, 493, 107, 19, 51], [20, 493, 107, 19, 50], [21, 493, 107, 19, 51], [22, 493, 108, 19, 50], [23, 493, 108, 19, 49], [24, 492, 108, 20, 50]]
Novamente, a haste do pêndulo é de $70\,\texttt{cm}$ contados da ponta de cima do fio até a centróide da bola, considerada como sendo o centro de massa do objeto, que por sua vez é tratado como um objeto pontual.
nd1 = np.array(tracado1[2:])
quadro1 = nd1[:,0]
x1 = nd1[:,1]
y1 = nd1[:,2]
w1 = nd1[:,3]
h1 = nd1[:,4]
plt.figure(figsize=(12,6))
plt.plot(quadro1, x1, '.', label='lado esquerdo')
plt.plot(quadro1, x1+w1, '.', label='lado direito')
plt.plot(quadro1, x1+w1/2, '-', label='média')
plt.title('Dados horizontais da caixa de entorno', fontsize=18)
plt.legend()
plt.show()
plt.figure(figsize=(12,4))
plt.plot(quadro1, y1, '.', label='lado superior')
plt.plot(quadro1, y1+h1, '.', label='lado inferior')
plt.plot(quadro1, y1+h1/2, '-', label='média')
plt.ylim(plt.ylim()[::-1])
plt.title('Dados verticais da caixa de entorno - primeiro vídeo do pêndulo', fontsize=18)
plt.legend()
plt.show()
Observação: A diferença entre o nível dos "picos" no gráfico acima parece ser uma questão de perspectiva, devida a uma oscilação do pêndulo em um plano não exatamente paralelo à placa de captura de imagem da câmera.
Novamente, o mesmo pêndulo, com os mesmo $70\,\texttt{cm}$ de haste, mas com a câmera em outra posição, um tempo maior de duração e com ângulo inicial um pouco diferente.
fps2, num_quadros2, tracado2 = movimento.detector_de_movimento(path.join(videos_dir, 'pendulo_70cm_2_reduzido.mov'))
print('Quadros por segundo:', fps2)
print('Número de quadros:', num_quadros2)
print(f"Tempo tratado de vídeo: {int(num_quadros2/fps2/60):3d}m {int(num_quadros2/fps2 % 60):02d}s {int(1000*(num_quadros2/fps2 % 1)):02d}ms")
print()
print('Dados iniciais do traçado:')
tracado2[1:11]
Quadros por segundo: 30.0 Número de quadros: 3632 Tempo tratado de vídeo: 2m 01s 66ms Dados iniciais do traçado:
[['quadro', 'abscissa(px)', 'ordenada(px)', 'largura(px)', 'altura(px)'], [130, 486, 0, 26, 37], [131, 486, 0, 26, 47], [132, 484, 0, 28, 57], [133, 482, 0, 30, 69], [134, 481, 0, 31, 80], [135, 480, 0, 32, 89], [136, 480, 0, 32, 97], [137, 479, 0, 33, 103], [138, 478, 0, 34, 109]]
nd2 = np.array(tracado2[2:])
quadro2 = nd2[:,0]
x2 = nd2[:,1]
y2 = nd2[:,2]
w2 = nd2[:,3]
h2 = nd2[:,4]
plt.figure(figsize=(12,6))
plt.plot(quadro2, x2, '.', label='lado esquerdo')
plt.plot(quadro2, x2+w2, '.', label='lado direito')
plt.plot(quadro2, x2+w2/2, '-', label='média')
plt.title('Dados horizontais da caixa de entorno - segundo vídeo do pêndulo', fontsize=18)
plt.legend()
plt.show()
plt.figure(figsize=(12,6))
plt.plot(quadro2[200:600], x2[200:600], '.', label='lado esquerdo')
plt.plot(quadro2[200:600], x2[200:600]+w2[200:600], '.', label='lado direito')
plt.plot(quadro2[200:600], x2[200:600]+w2[200:600]/2, '-', label='média')
plt.title('Dados horizontais da caixa de entorno - segundo vídeo do pêndulo', fontsize=18)
plt.legend()
plt.show()
plt.figure(figsize=(12,6))
plt.plot(quadro2[200:600], x2[200:600]+w2[200:600]/2, '-', label='média')
plt.title('Dados verticais da caixa de entorno - segundo vídeo do pêndulo', fontsize=18)
plt.legend()
plt.show()
plt.figure(figsize=(12,6))
plt.plot(quadro2[200:600], y2[200:600], '.', label='lado superior')
plt.plot(quadro2[200:600], y2[200:600]+h2[200:600], '.', label='lado inferior')
plt.plot(quadro2[200:600], y2[200:600]+h2[200:600]/2, '-', label='média')
plt.ylim(plt.ylim()[::-1])
plt.title('Dados verticais da caixa de entorno - segundo vídeo do pêndulo', fontsize=18)
plt.legend()
plt.show()
Desta vez, usamos um pêndulo com $40\,\texttt{cm}$ de haste e um ângulo inicial bem maior de quase $90^\circ$ (na verdade, quase $-90^\circ$, segundo a orientação usual).
Vamos, apenas, visualizar o trecho inicial do vídeo. Deixamos o gráfico do traçado por conta de vocês.
fps3, num_quadros3, tracado3 = movimento.detector_de_movimento(path.join(videos_dir, 'pendulo_40cm_reduzido.mov'),
video_tracado = path.join(videos_dir, 'pendulo_40cm_reduzido_tracado_inicio.avi'),
quadro_final = 150)
video = io.open(path.join(videos_dir, 'pendulo_40cm_reduzido_tracado_inicio.avi'), 'r+b').read()
encoded = base64.b64encode(video)
HTML(data='''<video width="80%" alt="test" controls><source src="data:video/avi;base64,{0}" type="video/avi" />
</video>'''.format(encoded.decode('ascii')))
Nos vídeos, só temos a informação do comprimento da haste do pêndulo e dos pontos, em pixels, da posição e do tamanho da caixa de entorno. É preciso passar isso para o ângulo $\theta(t)$ da posição do pêndulo. Isso pode ser feito com um pouco de geometria.
Na imagem da câmera, o objeto aparenta ser menor quando está mais afastado da câmera. Isso causa desvios na leitura da posição do objeto. Mais significativo para isso é o fato da câmera não estar bem alinhada perpendicularmente ao plano de oscilação do pêndulo. E o pêndulo, aliás, não oscila exatamente em um plano.
Para corrigir esses desvios, é necessário usar uma transformação afim correspondente à projeção do objeto no plano do sensor de captação de imagem da câmera. Pode ser feita uma aproximação por uma transformação afim bidimensional mais simples ou usar uma formulação mais completa, como a descrita em 3D projection: Mathematical Formula
Estender a análise do pêndulo para o caso de oscilações com grandes amplitudes, conforme discute nesta aula e na anterior. Em particular, deve conter
Um ajuste, aos dados capturados pelos vídeos acima, por um modelo de EDO incluindo um ou mais termos relativo à resistência do ar.
Analisar a dependência do período na amplitude a partir dos dados capturados pelo vídeo e comparar com a análise teórica feita na aula anterior.
Enviar arquivo no formato de um caderno Jupyter.
Enviar por correio eletrônico com cópia para Alejandro Cabrera e Ricardo Rosa.
Enviar a mensagem com o assunto Projeto 2 de Modelagem Matemática: Pêndulo com oscilações de grande amplitude.
Nomear o arquivo do caderno Jupyter com o número do projeto e o nome do(a) aluno(a), e.g. Projeto2_Nome_do_Estudante.ipynb.
Imagens ou dados que precisem ser carregados pelo caderno devem também ser enviados por correio eletrônico, em um subdiretório.
Podem enviar o caderno e o subdiretório agrupados em um único arquivo .tar, .gz ou semelhante.
O caderno Jupyter deve conter as seguintes informações: